package pers.yurwisher.rubick.oauth2.config;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.crypto.digest.MD5;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import pers.yurwisher.rubick.oauth2.common.CustomOAuth2AccessToken;
import pers.yurwisher.rubick.oauth2.exception.OAuth2ExceptionHandler;
import pers.yurwisher.rubick.oauth2.security.OAuth2User;
import pers.yurwisher.rubick.oauth2.service.CustomClientDetailsService;
import pers.yurwisher.rubick.oauth2.service.RedisAuthorizationCodeServices;
import pers.yurwisher.rubick.oauth2.support.CustomTokenStoreSerializationStrategy;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/**
 * @author yq 2021/4/15 13:43
 * @description OAuth2 配置
 */
@Configuration
@EnableAuthorizationServer
@RequiredArgsConstructor
@Slf4j
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    private static final MD5 MD_5 = new MD5(OAuth2ServerConfig.class.getName().getBytes());

    private final RedisConnectionFactory redisConnectionFactory;

    private final UserDetailsService userDetailsService;
    private final AuthenticationManager authenticationManager;
    private final CustomClientDetailsService clientDetailsService;
    private final RedisTemplate<String, Object> redisTemplate;
    private final OAuth2ExceptionHandler oAuth2ExceptionHandler;


    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //支持client_id以及client_secret作登录认证
        security.allowFormAuthenticationForClients();
        security.checkTokenAccess("permitAll()");
        security.tokenKeyAccess("isAuthenticated()");

    }

    /**
     * 配置客户端详情 服务,获取oauth2对应的应用
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 配置token存储，一般配置redis存储
        endpoints.tokenStore(tokenStore())
                // 自定义token
                .tokenEnhancer(tokenEnhancer())
                // 配置认证管理器
                .authenticationManager(authenticationManager)
                // 配置用户详情server，密码模式必须
                .userDetailsService(userDetailsService)
                // 配置授权码模式授权码服务,不配置默认为内存模式
                .authorizationCodeServices(authorizationCodeServices())
                //默认配置支持的grant_type模式 AuthorizationServerEndpointsConfigurer.getDefaultTokenGranters
                .tokenGranter(tokenGranter(endpoints));
        // 配置TokenServices参数
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        // 是否支持刷新Token
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(true);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        // 设置accessToken和refreshToken的默认超时时间(如果clientDetails的为null就取默认的，如果clientDetails的不为null取clientDetails中的)
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(2));
        tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
        endpoints.tokenServices(tokenServices);
        endpoints.exceptionTranslator(oAuth2ExceptionHandler);
    }

    /**
     * 配置token存储，这个配置token存到redis中
     *
     * @return tokenStore
     */
    @Bean
    public TokenStore tokenStore() {
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        redisTokenStore.setAuthenticationKeyGenerator(keyGenerator());
        //自定义缓存序列化策略
        redisTokenStore.setSerializationStrategy(new CustomTokenStoreSerializationStrategy(redisTemplate));
        return redisTokenStore;
    }


    /**
     * 自定义token唯一key
     *
     * @return AuthenticationKeyGenerator
     */
    private AuthenticationKeyGenerator keyGenerator() {
        return new AuthenticationKeyGenerator() {
            /**
             * @param authentication an OAuth2Authentication
             * @return a unique key identifying the authentication
             */
            @Override
            public String extractKey(OAuth2Authentication authentication) {
                OAuth2Request oAuth2Request = authentication.getOAuth2Request();
                String clientId = oAuth2Request.getClientId();
                OAuth2User principal = (OAuth2User) authentication.getPrincipal();
                //userId + clientId 组成
                String value = principal.getUser().getId() + clientId;
                byte[] bytes = MD_5.digest(value.getBytes(StandardCharsets.UTF_8));
                return String.format("%032x", new BigInteger(1, bytes));
            }
        };
    }

    /**
     * 配置授权码模式授权码服务,不配置默认为内存模式
     *
     * @return 授权码服务
     */
    @Primary
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new RedisAuthorizationCodeServices(redisTemplate, 300L);
    }

    /**
     * token增强,添加一些元信息,包装响应结果
     *
     * @return TokenEnhancer
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> new CustomOAuth2AccessToken(accessToken);
    }

    /**
     * 创建grant_type列表
     * 如果不配置则默认使用密码模式、简化模式、验证码模式以及刷新token模式，如果配置了只使用配置中，默认配置失效
     * 体可以查询 AuthorizationServerEndpointsConfigurer 中的 getDefaultTokenGranters 方法
     */
    public TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
        AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
        ClientDetailsService clientDetailsService = endpoints.getClientDetailsService();
        OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();
        return new CompositeTokenGranter(CollectionUtil.newArrayList(
                //密码模式
                new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory),
                //刷新token模式
                new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory),
                //授权码模式
                new AuthorizationCodeTokenGranter(tokenServices, endpoints.getAuthorizationCodeServices(), clientDetailsService, requestFactory),
                //简化模式
                new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory)
        ));
    }
}
